﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Roslyn.Compilers.CSharp;
using PathSplitterUtil;

namespace PathSplitter
{

    public static class Parser
    {
        public static StatementPathCollection<T> ParsePaths<T>(string code)
        {
            Compilation c = RosylnUtil.CompileCode(code);
            return ParsePaths<T>(c: c);
        }
        public static StatementPathCollection<T> ParsePaths<T>(Compilation c)
        {
            var root = c.SyntaxTrees.Single().GetRoot();
            var usings = string.Join("\n", root.ChildNodes().OfType<UsingDirectiveSyntax>()) + "\n";

            var method = root.ChildNodes().OfType<MethodDeclarationSyntax>().FirstOrDefault();
            if (method == null)
            {
                var m = root.ChildNodes().OfType<ClassDeclarationSyntax>().FirstOrDefault();
                if (m == null)
                {
                    throw new ArgumentException();
                }
                return ParsePaths<T>(m);
            }
            else
            {
                return ParsePaths<T>(method);
            }
        }
        public static StatementPathCollection<T> ParsePaths<T>(this ClassDeclarationSyntax cls)
        {
            string clsStr = cls.ToString();
            clsStr = clsStr.Substring(0, clsStr.IndexOf('{')).Trim();
            var method = cls.Members.OfType<MethodDeclarationSyntax>().FirstOrDefault();
            if (method == null)
            {
                throw new ArgumentException();
            }
            var methodPath = method.ParsePaths<T>();
            return new IndentedPathCollection<T>(header: clsStr + "\n{\n\t", footer: "\n}", statement: methodPath);
        }
        public static MethodPathCollection<T> ParsePaths<T>(this MethodDeclarationSyntax method)
        {
            var bodyPath = method.Body._ParsePaths<T>();
            // Special because we have to be able to add arguments to the method
            return new MethodPathCollection<T>(method: method, body: bodyPath);
        }
        private static StatementPathCollection<T> _ParsePaths<T>(this StatementSyntax code)
        {
            if (!code.ChildNodes().Any()
                || code is GotoStatementSyntax)
            {
                // leaf nodes
                return new SimpleStatementPathCollection<T>(code: code.GetText());
            }
            else if (code is BlockSyntax)
            {
                var block = (BlockSyntax)code;
                var statementPaths = block.Statements.Select(s => s._ParsePaths<T>()).ToList();
                return new IndentedPathCollection<T>(header: block.OpenBraceToken.ValueText + (statementPaths.Any() ? "\n\t" : ""),
                    footer: "\n" + block.CloseBraceToken.ValueText,
                    statements: statementPaths.ToList());
            }
            else if (code is IfStatementSyntax)
            {
                var ifStatement = (IfStatementSyntax)code;

                return new IfPathCollection<T>(condition: ifStatement.Condition._ParsePaths<T>(),
                    ifBlock: ifStatement.Statement._ParsePaths<T>(),
                    elseBlock: ifStatement.Else == null
                                        ? null
                                        : ifStatement.Else.Statement._ParsePaths<T>());
            }
            else if (code is SwitchStatementSyntax)
            {
                var sw = (SwitchStatementSyntax)code;

                var expressionPaths = sw.Expression._ParsePaths<T>();
                var sections = sw.Sections.Select(
                    s => new SwitchSectionPathCollection<T>(
                        s.Labels.Select(sl => sl.Value == null
                            ? null
                            : sl.Value.GetText()).ToList(),
                        new IndentedPathCollection<T>("", "", s.Statements.Select(st => st._ParsePaths<T>()).ToList())))
                   .ToList();
                return new SwitchPathCollection<T>(expressionPaths, sections);
            }
        else if (code is WhileStatementSyntax || code is DoStatementSyntax
                || code is ForStatementSyntax)
        {
            if (code is WhileStatementSyntax)
            {
                var whileStatement = (WhileStatementSyntax)code;

                return new WhilePathCollection<T>(
                    loopCondition: whileStatement.Condition._ParsePaths<T>(),
                    loopBody: whileStatement.Statement._ParsePaths<T>());
            }
            else if (code is DoStatementSyntax)
            {
                var doStatement = (DoStatementSyntax)code;
                
                return new DoWhilePathCollection<T>(
                    loopCondition: doStatement.Condition._ParsePaths<T>(),
                    loopBody: doStatement.Statement._ParsePaths<T>());
            }
            else if (code is ForStatementSyntax)
            {
                var forStatement = (ForStatementSyntax)code;
                return new ForPathCollection<T>(
                    loopBody: forStatement.Statement._ParsePaths<T>(),
                    loopDecls: forStatement.Declaration != null ? forStatement.Declaration._ParsePaths<T>() : null,
                    loopCondition: forStatement.Condition._ParsePaths<T>(),
                    loopIncrementors: forStatement.Incrementors != null
                        ? forStatement.Incrementors.Select(i => i._ParsePaths<T>()).ToList()
                        : new List<ExpressionPathCollection<T>>());
            }
            else
            {
                throw new NotImplementedException("Unsupported loop type: " + code.GetType() + ": " + code);
            }
        }
            else if (code is ForEachStatementSyntax)
            {
                var forEach = (ForEachStatementSyntax)code;

                string loopVar = forEach.Identifier.GetText();
                string baseEnumeratorName = "__" + loopVar + "__enumerator_";
                string enumeratorName = baseEnumeratorName;
                int i = 0;
                string fullTreeText = code.SyntaxTree.GetRoot().GetText();
                while (fullTreeText.Contains(enumeratorName))
                {
                    enumeratorName = baseEnumeratorName + (i++);
                }

                return new ForEachPathCollection<T>(
                    expression: forEach.Expression._ParsePaths<T>(),
                    loopBody: forEach.Statement._ParsePaths<T>(),
                    typeName: forEach.Type.GetText(),
                    loopVar: loopVar,
                    enumeratorName: enumeratorName);
            }
            else if (code is LocalDeclarationStatementSyntax)
            {
                var decl = (LocalDeclarationStatementSyntax)code;
                return new NormalStatementPathCollection<T>(
                    string.Join(" ", decl.Modifiers) + "{0}" + decl.SemicolonToken.GetText(),
                    decl.Declaration._ParsePaths<T>());
            }
            else if (code is ReturnStatementSyntax)
            {
                var ret = (ReturnStatementSyntax)code;
                if (ret.Expression == null)
                {
                    return new SimpleStatementPathCollection<T>(code.GetText().Trim());
                }
                else
                {
                    return new NormalStatementPathCollection<T>(ret.ReturnKeyword.GetText()
                            + " {0}" + ret.SemicolonToken.GetText(), ret.Expression._ParsePaths<T>());
                }
            }
            else if (code is YieldStatementSyntax)
            {
                var yieldStatement = (YieldStatementSyntax)code;
                if (yieldStatement.Expression == null)
                {
                    return new SimpleStatementPathCollection<T>(code.GetText().Trim());
                }
                else
                {
                    return new NormalStatementPathCollection<T>(yieldStatement.YieldKeyword.GetText()
                            + " " + yieldStatement.ReturnOrBreakKeyword.GetText()
                            + " " + "{0}" + yieldStatement.SemicolonToken.GetText(),
                            yieldStatement.Expression._ParsePaths<T>());
                }
            }
            else if (code is ThrowStatementSyntax)
            {
                var throwStatement = (ThrowStatementSyntax)code;
                if (throwStatement.Expression == null)
                {
                    return new SimpleStatementPathCollection<T>(code.GetText().Trim());
                }
                else
                {
                    return new NormalStatementPathCollection<T>(
                            throwStatement.ThrowKeyword.GetText()
                                + " " + "{0}" + throwStatement.SemicolonToken.GetText(),
                            throwStatement.Expression._ParsePaths<T>());
                }
            }
            else if (code is ExpressionStatementSyntax)
            {
                var statement = (ExpressionStatementSyntax)code;
                return new NormalStatementPathCollection<T>(
                    "{0}" + statement.SemicolonToken.GetText(),
                    statement.Expression._ParsePaths<T>());
            }
            else if (code is CheckedStatementSyntax)
            {
                var check = (CheckedStatementSyntax)code;
                return new NormalStatementPathCollection<T>(
                    check.Keyword.GetText() + "\n" + "{0}",
                    check.Block._ParsePaths<T>());
            }
            else if (code is LabeledStatementSyntax)
            {
                var labeled = (LabeledStatementSyntax)code;
                return new NormalStatementPathCollection<T>(
                        labeled.Identifier.GetText()
                                + labeled.ColonToken
                                + "{0}",
                        labeled.Statement._ParsePaths<T>());
            }
            else if (code is TryStatementSyntax)
            {
                var tryStatement = (TryStatementSyntax)code;

                IList<StatementPathCollection<T>> blocks
                    = new List<StatementPathCollection<T>>();

                blocks.Add(tryStatement.Block._ParsePaths<T>());
                string tryStr = tryStatement.TryKeyword.GetText() + " "
                                + "{0}" + " ";
                int formatIdx = 1;

                foreach (var c in tryStatement.Catches)
                {
                    string catchPrefix = c.ToString();
                    catchPrefix = catchPrefix
                                 .Substring(0, catchPrefix.IndexOf('{'))
                                 .Trim()
                                 + " ";
                    tryStr += catchPrefix + "{" + (formatIdx++) + "} ";
                    blocks.Add(c.Block._ParsePaths<T>());
                }


                if (tryStatement.Finally != null)
                {
                    blocks.Add(tryStatement.Finally.Block._ParsePaths<T>());
                    tryStr += tryStatement.Finally.FinallyKeyword + "{" + (formatIdx++) + "}";
                }

                return new NormalStatementPathCollection<T>(tryStr.Trim(), blocks.ToArray());
            }
            else
            {
                throw new NotImplementedException("Unknown syntax node type: " + code.GetType() + ": " + code);
                //return new[] { code.ToString() };
            }
        }

        private static ExpressionPathCollection<T> _ParsePaths<T>(this ExpressionSyntax code)
        {
            if (!code.ChildNodes().Any()
                || code is DefaultExpressionSyntax
                || code is TypeOfExpressionSyntax
                || (code is TypeSyntax && !(code is ArrayTypeSyntax))
                // TODO Right way to handle lambdas?
                || code is SimpleLambdaExpressionSyntax
                || code is ParenthesizedLambdaExpressionSyntax
                || code is AnonymousMethodExpressionSyntax
                // TODO Related... right way to handle LINQ queries?
                || code is QueryExpressionSyntax
                )
            {
                // leaf nodes
                return new SimpleExpressionPathCollection<T>(code.GetText());
            }
            else if (code is ConditionalExpressionSyntax)
            {
                var cond = (ConditionalExpressionSyntax)code;

                return new ConditionalExpressionPathCollection<T>(
                    condition: cond.Condition._ParsePaths<T>(),
                    whenTrue: cond.WhenTrue._ParsePaths<T>(),
                    whenFalse: cond.WhenFalse._ParsePaths<T>());
            }
            else if (code is PrefixUnaryExpressionSyntax)
            {
                var pre = (PrefixUnaryExpressionSyntax)code;
                return new NormalExpressionPathCollection<T>(
                    pre.OperatorToken.GetText() + "{0}", pre.Operand._ParsePaths<T>());
            }
            else if (code is PostfixUnaryExpressionSyntax)
            {
                var post = (PostfixUnaryExpressionSyntax)code;
                return new NormalExpressionPathCollection<T>(
                    "{0}" + post.OperatorToken.GetText(), post.Operand._ParsePaths<T>());
            }
            else if (code is ParenthesizedExpressionSyntax)
            {
                var expr = (ParenthesizedExpressionSyntax)code;
                return new NormalExpressionPathCollection<T>(
                    expr.OpenParenToken + "{0}" + expr.CloseParenToken.GetText(),
                    expr._ParsePaths<T>());
            }
            else if (code is CastExpressionSyntax)
            {
                var cast = (CastExpressionSyntax)code;
                return new NormalExpressionPathCollection<T>(
                    cast.OpenParenToken.ToString() + cast.Type + cast.CloseParenToken + "{0}",
                    cast.Expression._ParsePaths<T>());
            }
            else if (code is BinaryExpressionSyntax)
            {
                var bin = (BinaryExpressionSyntax)code;
                return new NormalExpressionPathCollection<T>("{0}"
                        + (bin.ToString().Contains(" " + bin.OperatorToken.GetText()) ? " " : "")
                        + bin.OperatorToken.GetText()
                        + (bin.ToString().Contains(bin.OperatorToken.GetText() + " ") ? " " : "")
                        + "{1}",
                    bin.Left._ParsePaths<T>(), bin.Right._ParsePaths<T>());
            }
            else if (code is MemberAccessExpressionSyntax)
            {
                var mem = (MemberAccessExpressionSyntax)code;
                return new NormalExpressionPathCollection<T>(
                    "{0}" + mem.OperatorToken + mem.Name,
                    mem.Expression._ParsePaths<T>());
            }
            else if (code is InvocationExpressionSyntax)
            {
                var call = (InvocationExpressionSyntax)code;
                var targets = call.Expression._ParsePaths<T>();
                var argumentList = call.ArgumentList.Arguments.Select(a => a._ParsePaths<T>()).ToList();

                return new NormalExpressionPathCollection<T>("{0}"
                            + call.ArgumentList.OpenParenToken
                            + string.Join(", ", argumentList.Select((_, i) => "{" + (i+1) + "}"))
                            + call.ArgumentList.CloseParenToken,
                        targets.Cons(argumentList).ToArray());
            }
            else if (code is ObjectCreationExpressionSyntax)
            {
                var newObj = (ObjectCreationExpressionSyntax)code;
                var argumentList = newObj.ArgumentList.Arguments.Select(a => a._ParsePaths<T>()).ToList();
                var initializerPaths = newObj.Initializer == null
                    ? (ExpressionPathCollection<T>)new SimpleExpressionPathCollection<T>()
                    : newObj.Initializer._ParsePaths<T>();
                if (newObj.Initializer != null)
                {
                    throw new NotImplementedException("Object initializer unsupported in " + newObj);
                }

                return new NormalExpressionPathCollection<T>(newObj.NewKeyword.ToString()
                                + newObj.Type.ToString()
                                + newObj.ArgumentList.OpenParenToken
                                + string.Join(", ", argumentList.Select((_, i) => "{" + i + "}"))
                                + newObj.ArgumentList.CloseParenToken,
                            argumentList.ToArray());
            }
            else if (code is AnonymousObjectCreationExpressionSyntax)
            {
                var newObj = (AnonymousObjectCreationExpressionSyntax)code;
                var initializerPaths = newObj.Initializers
                    .Select(initializer => {
                        var baseInit = initializer.Expression._ParsePaths<T>();
                        return initializer.NameEquals == null
                            ? baseInit
                            : new NormalExpressionPathCollection<T>(initializer.NameEquals.ToString() + "{0}", baseInit);
                    })
                    .ToArray();

                return new NormalExpressionPathCollection<T>(
                            newObj.NewKeyword.ToString()
                                + newObj.OpenBraceToken.ToString().Replace("{", "{{")
                                + string.Join(", ", initializerPaths.Select((_, i) => "{" + i + "}"))
                                + newObj.CloseBraceToken.ToString().Replace("}", "}}"),
                            initializerPaths);
            }
            else if (code is ElementAccessExpressionSyntax)
            {
                var access = (ElementAccessExpressionSyntax)code;
                var target = access.Expression._ParsePaths<T>();
                var argumentLists = access.ArgumentList.Arguments.Select(a => a._ParsePaths<T>()).ToList();

                return new NormalExpressionPathCollection<T>("{0}"
                                + access.ArgumentList.OpenBracketToken
                                + string.Join(", ", argumentLists.Select((_, i) => "{" + (i+1) + "}"))
                                + access.ArgumentList.CloseBracketToken,
                            target.Cons(argumentLists).ToArray());
            }
            else if (code is ArrayCreationExpressionSyntax)
            {
                var ac = (ArrayCreationExpressionSyntax)code;
                var typePaths = ac.Type._ParsePaths<T>();
                return ac.Initializer == null
                    ? new NormalExpressionPathCollection<T>(ac.NewKeyword.ToString() + "{0}", typePaths)
                    : new NormalExpressionPathCollection<T>(ac.NewKeyword.ToString() + "{0}{1}", typePaths, ac.Initializer._ParsePaths<T>());
            }
            else if (code is ImplicitArrayCreationExpressionSyntax)
            {
                var ac = (ImplicitArrayCreationExpressionSyntax)code;
                return ac.Initializer == null
                    ? (ExpressionPathCollection<T>)new SimpleExpressionPathCollection<T>(ac.NewKeyword.ToString()
                        + ac.OpenBracketToken + ac.CloseBracketToken)
                    : new NormalExpressionPathCollection<T>(ac.NewKeyword.ToString()
                        + ac.OpenBracketToken + ac.CloseBracketToken
                        + "{0}",
                        ac.Initializer._ParsePaths<T>());
            }
            else if (code is ArrayTypeSyntax)
            {
                var at = (ArrayTypeSyntax)code;

                string str = at.PlainName;
                int idx = 0;
                List<ExpressionPathCollection<T>> args = new List<ExpressionPathCollection<T>>();
                foreach (var r in at.RankSpecifiers)
                {
                    str += r.OpenBracketToken;
                    var sizeExprs = r.Sizes.Select(s => s._ParsePaths<T>()).ToList();
                    str += string.Join(", ", Enumerable.Range(idx, sizeExprs.Count).Select(i => "{" + i + "}"));
                    idx += sizeExprs.Count;
                    args.AddRange(sizeExprs);
                    str += r.CloseBracketToken;
                }
                return new NormalExpressionPathCollection<T>(str, args.ToArray());
            }
            else if (code is InitializerExpressionSyntax)
            {
                var init = (InitializerExpressionSyntax)code;
                var inits = init.Expressions.Select(e => e._ParsePaths<T>()).ToArray();
                return new NormalExpressionPathCollection<T>(string.Join(", ", inits.Select((_, i) => "{" + i + "}")), inits);
            }
            else if (code is CheckedExpressionSyntax)
            {
                var check = (CheckedExpressionSyntax)code;
                return new NormalExpressionPathCollection<T>(check.Keyword.GetText()
                                                              + check.OpenParenToken.GetText()
                                                              + "{0}"
                                                              + check.CloseParenToken.GetText(),
                                                             check.Expression._ParsePaths<T>());
            }
            else
            {
                throw new NotImplementedException("Unknown expression type: " + code.GetType() + ": " + code);
            }
        }
        private static ExpressionPathCollection<T> _ParsePaths<T>(this ArgumentSyntax arg)
        {
            return new NormalExpressionPathCollection<T>(
                (arg.NameColon == null ? "" : arg.NameColon.ToString())
                    + arg.RefOrOutKeyword.ToString()
                    + " {0}",
                arg.Expression._ParsePaths<T>());
        }
        private static ExpressionPathCollection<T> _ParsePaths<T>(this VariableDeclarationSyntax decl)
        {
            var decls = decl.Variables.Select(v => v._ParsePaths<T>()).ToArray();
            return new NormalExpressionPathCollection<T>(decl.Type.GetText() + " " + string.Join(", ",
                                                            decls.Select((_, i) => "{" + i + "}")),
                                                         decls);
        }
        private static ExpressionPathCollection<T> _ParsePaths<T>(this VariableDeclaratorSyntax decl)
        {
            if (decl.Initializer == null)
            {
                return new SimpleExpressionPathCollection<T>(decl.Identifier.GetText());
            }
            else
            {
                return new NormalExpressionPathCollection<T>(decl.Identifier + "{0}", decl.Initializer._ParsePaths<T>());
            }
        }
        private static ExpressionPathCollection<T> _ParsePaths<T>(this EqualsValueClauseSyntax decl)
        {
            return new NormalExpressionPathCollection<T>(decl.EqualsToken + "{0}", decl.Value._ParsePaths<T>());
        }
    }
}
